package com.dbms.catalog;

import com.dbms.common.Column;
import com.dbms.common.Schema;
import com.dbms.common.Tuple;
import com.dbms.common.Type;
import com.dbms.parser.ast.ForeignKeyNode;
import com.dbms.storage.BPlusTree;
import com.dbms.storage.BPlusTreePage;
import com.dbms.storage.BufferPoolManager;
import com.dbms.storage.Page;
import com.dbms.storage.TablePage;

import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Catalog类负责管理数据库的元数据信息，包括表结构、列信息、用户信息和权限信息等。
 * 它通过缓冲池管理器(BufferPoolManager)与存储层交互，维护元数据的持久化和内存缓存。
 */
public class Catalog {

    // 缓冲池管理器，用于获取、管理数据库页
    private final BufferPoolManager bufferPoolManager;
    // 表名到表元数据的映射，内存中缓存所有表的元数据
    private final Map<String, TableMetadata> tableMetadataMap;

    // 系统目录表_tables_的 schema（存储所有表的基本信息）
    private final Schema tableCatalogSchema;
    // 系统目录表_columns_的 schema（存储所有列的详细信息）
    private final Schema columnCatalogSchema;
    // 系统目录表_users_的 schema（存储用户信息）
    private final Schema userCatalogSchema;
    // 系统目录表_privileges_的 schema（存储权限信息）
    private final Schema privilegeCatalogSchema;

    // 各系统目录表的根页ID（固定值，用于定位目录表在存储中的位置）
    private static final int TABLES_CATALOG_ROOT_PAGE_ID = 0;      // _tables_表的根页ID
    private static final int COLUMNS_CATALOG_ROOT_PAGE_ID = 1;     // _columns_表的根页ID
    private static final int USERS_CATALOG_ROOT_PAGE_ID = 2;       // _users_表的根页ID
    private static final int PRIVILEGES_CATALOG_ROOT_PAGE_ID = 3;  // _privileges_表的根页ID

    /**
     * 初始化Catalog实例
     * @param bpm 缓冲池管理器，用于与存储层交互
     * @param isNewDb 是否为新数据库。true则初始化系统目录表，false则从存储加载已有目录
     * @throws IOException 当与存储层交互（如获取页）失败时抛出
     */
    public Catalog(BufferPoolManager bpm, boolean isNewDb) throws IOException {
        this.bufferPoolManager = bpm;
        this.tableMetadataMap = new HashMap<>();

        // 初始化_tables_表的schema：存储表名、根页ID、索引根页ID
        this.tableCatalogSchema = new Schema(List.of(
                new Column("table_name", Type.VARCHAR, 32),       // 表名
                new Column("root_page_id", Type.INTEGER),          // 表数据的根页ID
                new Column("index_root_page_id", Type.INTEGER)     // 表索引的根页ID
        ));

        // 初始化_columns_表的schema：存储列所属表、列名、类型、位置等信息
        this.columnCatalogSchema = new Schema(List.of(
                new Column("table_name", Type.VARCHAR, 32),       // 列所属的表名
                new Column("column_name", Type.VARCHAR, 32),      // 列名
                new Column("column_type", Type.INTEGER),          // 列类型（0表示INTEGER，1表示VARCHAR）
                new Column("column_index", Type.INTEGER),         // 列在表中的位置索引
                new Column("varchar_length", Type.INTEGER),       // 若为VARCHAR类型，存储其长度
                new Column("is_primary_key", Type.INTEGER)        // 是否为主键（1是，0否）
        ));

        // 初始化_users_表的schema：存储用户名和密码哈希
        this.userCatalogSchema = new Schema(List.of(
                new Column("user_name", Type.VARCHAR, 32, true),  // 用户名（主键）
                new Column("password_hash", Type.VARCHAR, 128)    // 密码的SHA-256哈希（Base64编码）
        ));

        // 初始化_privileges_表的schema：存储用户对表的权限
        this.privilegeCatalogSchema = new Schema(List.of(
                new Column("user_name", Type.VARCHAR, 32),       // 用户名
                new Column("table_name", Type.VARCHAR, 32),      // 表名
                new Column("privilege", Type.VARCHAR, 16)        // 权限类型（如SELECT、INSERT等）
        ));

        // 根据是否为新数据库，决定初始化或加载目录
        if (isNewDb) {
            initializeCatalog();
        } else {
            loadCatalog();
        }
    }

    /**
     * 初始化新数据库的系统目录表。
     * 创建_tables_、_columns_、_users_、_privileges_四个系统表，并将它们的元数据写入存储。
     * @throws IOException 当创建页或写入数据失败时抛出
     */
    private void initializeCatalog() throws IOException {
        // 为四个系统目录表分配新页并初始化
        Page tablesCatalogPage = bufferPoolManager.newPage();
        tablesCatalogPage.init();

        Page columnsCatalogPage = bufferPoolManager.newPage();
        columnsCatalogPage.init();

        Page usersCatalogPage = bufferPoolManager.newPage();
        usersCatalogPage.init();

        Page privilegesCatalogPage = bufferPoolManager.newPage();
        privilegesCatalogPage.init();

        // 将系统目录表自身的元数据写入_tables_和_columns_（内部创建逻辑）
        createTableInternal("_tables_", tableCatalogSchema, TABLES_CATALOG_ROOT_PAGE_ID, null, new ArrayList<>());
        createTableInternal("_columns_", columnCatalogSchema, COLUMNS_CATALOG_ROOT_PAGE_ID, null, new ArrayList<>());
        createTableInternal("_users_", userCatalogSchema, USERS_CATALOG_ROOT_PAGE_ID, null, new ArrayList<>());
        createTableInternal("_privileges_", privilegeCatalogSchema, PRIVILEGES_CATALOG_ROOT_PAGE_ID, null, new ArrayList<>());

        // 解锁并标记这些页为脏页（需要写回磁盘）
        bufferPoolManager.unpinPage(TABLES_CATALOG_ROOT_PAGE_ID, true);
        bufferPoolManager.unpinPage(COLUMNS_CATALOG_ROOT_PAGE_ID, true);
        bufferPoolManager.unpinPage(USERS_CATALOG_ROOT_PAGE_ID, true);
        bufferPoolManager.unpinPage(PRIVILEGES_CATALOG_ROOT_PAGE_ID, true);
    }

    /**
     * 从存储中加载已有数据库的目录信息。
     * 从_tables_表加载所有表的基本信息，再从_columns_表加载每个表的列信息，构建内存中的元数据缓存。
     * @throws IOException 当读取存储中的目录表失败时抛出
     */
    private void loadCatalog() throws IOException {
        // 从_tables_表加载所有表的基本信息（表名、根页ID、索引根页ID）
        Page tablesCatalogPage = bufferPoolManager.fetchPage(TABLES_CATALOG_ROOT_PAGE_ID);
        TablePage tablesPage = new TablePage(tablesCatalogPage);

        for (int i = 0; i < tablesPage.getSlotCount(); i++) {
            byte[] tupleData = tablesPage.getTuple(i);
            if (tupleData == null) continue;  // 跳过空槽位

            // 解析元组数据为表信息
            Tuple tuple = new Tuple(tupleData, tableCatalogSchema);
            String tableName = (String) tuple.getValue(0);
            int rootPageId = (Integer) tuple.getValue(1);
            int indexRootPageId = (Integer) tuple.getValue(2);
            BPlusTree index = new BPlusTree(bufferPoolManager, indexRootPageId, 50, 50);

            // 先暂存元数据（schema后续从_columns_加载）
            tableMetadataMap.put(tableName, new TableMetadata(tableName, null, rootPageId, index, false, null, new ArrayList<>()));
        }
        // 解锁_tables_表页（未修改，标记为非脏）
        bufferPoolManager.unpinPage(TABLES_CATALOG_ROOT_PAGE_ID, false);

        // 为每个表加载schema（列信息）并完善元数据
        for (String tableName : new ArrayList<>(tableMetadataMap.keySet())) {
            TableMetadata meta = tableMetadataMap.get(tableName);
            Schema schema = loadSchemaForTable(tableName);  // 从_columns_表加载列信息

            // 确定表是否分片（仅用户表，且主键为INTEGER时分片）
            String shardingKey = null;
            boolean isSharded = false;
            if (!tableName.startsWith("_")) {  // 系统表（_开头）不分片
                for (Column col : schema.getColumns()) {
                    if (col.isPrimaryKey() && col.getColumnType() == Type.INTEGER) {
                        isSharded = true;
                        shardingKey = col.getColumnName();
                        break;
                    }
                }
            }
            // 更新元数据（补充schema、分片信息）
            tableMetadataMap.put(tableName, new TableMetadata(tableName, schema, meta.rootPageId, meta.index, isSharded, shardingKey, meta.foreignKeys));
        }
    }

    /**
     * 为指定表加载其schema（列信息）。
     * 从_columns_系统表中查询属于该表的所有列，构建Schema对象。
     * @param tableName 要加载schema的表名
     * @return 该表的Schema对象
     * @throws IOException 当读取_columns_表失败时抛出
     */
    private Schema loadSchemaForTable(String tableName) throws IOException {
        List<Column> columns = new ArrayList<>();
        // 获取_columns_表的页并解析
        Page columnsCatalogPage = bufferPoolManager.fetchPage(COLUMNS_CATALOG_ROOT_PAGE_ID);
        TablePage columnsPage = new TablePage(columnsCatalogPage);

        for (int i = 0; i < columnsPage.getSlotCount(); i++) {
            byte[] tupleData = columnsPage.getTuple(i);
            if (tupleData == null) continue;  // 跳过空槽位

            // 解析元组数据为列信息
            Tuple tuple = new Tuple(tupleData, columnCatalogSchema);
            if (tuple.getValue(0).equals(tableName)) {  // 只处理当前表的列
                String columnName = (String) tuple.getValue(1);
                int columnTypeInt = (Integer) tuple.getValue(2);
                int varcharLength = (Integer) tuple.getValue(4);
                int isPrimaryKeyInt = (Integer) tuple.getValue(5);
                boolean isPrimaryKey = (isPrimaryKeyInt == 1);

                Type colType = columnTypeInt == 0 ? Type.INTEGER : Type.VARCHAR;

                // 创建Column对象（根据类型处理长度）
                Column col;
                if (colType == Type.INTEGER) {
                    col = new Column(columnName, colType, isPrimaryKey);
                } else {
                    col = new Column(columnName, colType, varcharLength, isPrimaryKey);
                }
                columns.add(col);
            }
        }
        // 解锁_columns_表页（未修改，标记为非脏）
        bufferPoolManager.unpinPage(COLUMNS_CATALOG_ROOT_PAGE_ID, false);
        return new Schema(columns);
    }

    /**
     * 公开接口：创建新表。
     * 检查表是否已存在，分配新页作为表的根页，调用内部方法完成表的创建（包括元数据写入系统目录）。
     * @param tableName 表名
     * @param schema 表的schema（列定义）
     * @param foreignKeyNodes 外键约束列表
     * @throws IOException 当创建页或写入元数据失败时抛出
     * @throws IllegalStateException 若表已存在则抛出
     */
    public void createTable(String tableName, Schema schema, List<ForeignKeyNode> foreignKeyNodes) throws IOException {
        if (tableMetadataMap.containsKey(tableName)) {
            throw new IllegalStateException("Table " + tableName + " already exists.");
        }

        // 为新表分配根页并初始化
        Page newTablePage = bufferPoolManager.newPage();
        newTablePage.init();
        int rootPageId = newTablePage.getPageId();
        bufferPoolManager.unpinPage(rootPageId, true);  // 解锁并标记为脏页

        // 创建表的索引（B+树）
        BPlusTree index = new BPlusTree(bufferPoolManager, BPlusTreePage.INVALID_PAGE_ID, 50, 50);

        // 调用内部方法完成表创建（写入元数据到系统目录）
        createTableInternal(tableName, schema, rootPageId, index, foreignKeyNodes);
    }

    /**
     * 更新表的索引根页ID。
     * 当索引结构发生变化（如分裂、合并）时，需同步更新目录中存储的索引根页ID。
     * @param tableName 表名
     * @param newIndexRootId 新的索引根页ID
     * @throws IOException 当读取或修改_tables_表失败时抛出
     */
    public void updateIndexRootId(String tableName, int newIndexRootId) throws IOException {
        Page tablesCatalogPage = bufferPoolManager.fetchPage(TABLES_CATALOG_ROOT_PAGE_ID);
        if (tablesCatalogPage == null) {
            throw new IOException("Could not fetch catalog page.");
        }
        tablesCatalogPage.wLock();  // 获取写锁，确保并发安全
        try {
            TablePage tablesPage = new TablePage(tablesCatalogPage);
            // 遍历_tables_表找到目标表，更新其索引根页ID
            for (int i = 0; i < tablesPage.getSlotCount(); i++) {
                byte[] tupleData = tablesPage.getTuple(i);
                if (tupleData == null) continue;

                Tuple tuple = new Tuple(tupleData, tableCatalogSchema);
                if (tuple.getValue(0).equals(tableName)) {
                    // 构建新元组（仅更新索引根页ID，其他字段不变）
                    List<Object> newValues = List.of(
                            tuple.getValue(0),  // 表名
                            tuple.getValue(1),  // 根页ID
                            newIndexRootId      // 新索引根页ID
                    );
                    Tuple newTuple = new Tuple(newValues, tableCatalogSchema);

                    tablesPage.updateTuple(i, newTuple.getData());  // 更新元组数据
                    break;
                }
            }
        } finally {
            tablesCatalogPage.wUnlock();  // 释放写锁
            bufferPoolManager.unpinPage(TABLES_CATALOG_ROOT_PAGE_ID, true);  // 标记为脏页
        }
    }

    /**
     * 内部方法：完成表的创建逻辑。
     * 将表信息写入_tables_目录表，将列信息写入_columns_目录表，更新内存元数据缓存。
     * @param tableName 表名
     * @param schema 表的schema
     * @param rootPageId 表数据的根页ID
     * @param index 表的索引（B+树）
     * @param foreignKeyNodes 外键约束列表
     * @throws IOException 当写入目录表失败时抛出
     */
    private void createTableInternal(String tableName, Schema schema, int rootPageId, BPlusTree index, List<ForeignKeyNode> foreignKeyNodes) throws IOException {
        // 1. 将表基本信息写入_tables_目录表
        Page tablesCatalogPage = bufferPoolManager.fetchPage(TABLES_CATALOG_ROOT_PAGE_ID);
        int indexRootId = (index != null) ? index.getRootPageId() : -1;  // 索引根页ID（无效则为-1）
        Tuple tableInfoTuple = new Tuple(List.of(tableName, rootPageId, indexRootId), tableCatalogSchema);
        new TablePage(tablesCatalogPage).insertTuple(tableInfoTuple.getData());  // 插入元组
        bufferPoolManager.unpinPage(TABLES_CATALOG_ROOT_PAGE_ID, true);  // 解锁并标记为脏页

        // 2. 将列信息写入_columns_目录表
        Page columnsCatalogPage = bufferPoolManager.fetchPage(COLUMNS_CATALOG_ROOT_PAGE_ID);
        TablePage columnsPage = new TablePage(columnsCatalogPage);
        int columnIndex = 0;  // 列在表中的位置索引（从0开始）
        String shardingKey = null;
        boolean isSharded = false;

        // 确定分片键（仅用户表处理）
        if (!tableName.startsWith("_")) {
            for (Column col : schema.getColumns()) {
                if (col.isPrimaryKey() && col.getColumnType() == Type.INTEGER) {
                    isSharded = true;
                    shardingKey = col.getColumnName();
                    break;
                }
            }
        }

        // 遍历所有列，插入到_columns_表
        for (Column col : schema.getColumns()) {
            int colTypeInt = col.getColumnType() == Type.INTEGER ? 0 : 1;  // 类型编码（0:INTEGER，1:VARCHAR）
            int varcharLen = col.getColumnType() == Type.VARCHAR ? col.getVarcharLength() : 0;
            int isPrimaryKeyInt = col.isPrimaryKey() ? 1 : 0;  // 主键标志（1:是，0:否）

            // 构建列信息元组
            // 修正后的代码
            Tuple columnInfoTuple = new Tuple(List.of(
                    tableName,
                    col.getColumnName(),
                    colTypeInt,
                    columnIndex,
                    varcharLen,
                    isPrimaryKeyInt
            ), columnCatalogSchema);
            columnsPage.insertTuple(columnInfoTuple.getData());  // 插入元组
            columnIndex++;
        }
        bufferPoolManager.unpinPage(COLUMNS_CATALOG_ROOT_PAGE_ID, true);  // 解锁并标记为脏页

        // 3. 处理外键约束（转换为内部ForeignKeyInfo）
        List<ForeignKeyInfo> foreignKeys = new ArrayList<>();
        for (ForeignKeyNode fkNode : foreignKeyNodes) {
            foreignKeys.add(new ForeignKeyInfo(
                    fkNode.getSourceColumn(),
                    fkNode.getReferencedTable(),
                    fkNode.getReferencedColumn()
            ));
        }

        // 4. 将表元数据加入内存缓存
        tableMetadataMap.put(tableName, new TableMetadata(
                tableName, schema, rootPageId, index, isSharded, shardingKey, foreignKeys
        ));
    }

    /**
     * 获取指定表的元数据。
     * @param tableName 表名
     * @return 表的元数据（TableMetadata对象），若表不存在则返回null
     */
    public TableMetadata getTable(String tableName) {
        return tableMetadataMap.get(tableName);
    }

    /**
     * 外键信息封装类，存储外键的源列、被引用表和被引用列。
     */
    public static class ForeignKeyInfo implements Serializable {
        public final String sourceColumnName;      // 源表中的外键列名
        public final String referencedTableName;   // 被引用的表名
        public final String referencedColumnName;  // 被引用表中的列名

        /**
         * 构造外键信息
         * @param sourceColumnName 源列名
         * @param referencedTableName 被引用表名
         * @param referencedColumnName 被引用列名
         */
        public ForeignKeyInfo(String sourceColumnName, String referencedTableName, String referencedColumnName) {
            this.sourceColumnName = sourceColumnName;
            this.referencedTableName = referencedTableName;
            this.referencedColumnName = referencedColumnName;
        }
    }

    /**
     * 表元数据封装类，存储表的所有元信息。
     */
    public static class TableMetadata {
        public final String tableName;             // 表名
        public Schema schema;                      // 表的schema（列定义）
        public final int rootPageId;               // 表数据的根页ID
        public final BPlusTree index;              // 表的索引（B+树）
        public final boolean isSharded;            // 是否为分片表
        public final String shardingKey;           // 分片键（若分片）
        public final List<ForeignKeyInfo> foreignKeys;  // 外键约束列表

        /**
         * 构造表元数据
         * @param tableName 表名
         * @param schema 表的schema
         * @param rootPageId 根页ID
         * @param index 索引
         * @param isSharded 是否分片
         * @param shardingKey 分片键
         * @param foreignKeys 外键列表
         */
        public TableMetadata(String tableName, Schema schema, int rootPageId, BPlusTree index, boolean isSharded, String shardingKey, List<ForeignKeyInfo> foreignKeys) {
            this.tableName = tableName;
            this.schema = schema;
            this.rootPageId = rootPageId;
            this.index = index;
            this.isSharded = isSharded;
            this.shardingKey = shardingKey;
            this.foreignKeys = foreignKeys;
        }
    }

    /**
     * 对密码进行SHA-256哈希处理，并返回Base64编码结果。
     * 用于安全存储密码（不存储明文）。
     * @param password 明文密码
     * @return 哈希后的密码（Base64编码）
     */
    public static String hashPassword(String password) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");  // 使用SHA-256算法
            byte[] hash = md.digest(password.getBytes(StandardCharsets.UTF_8));  // 计算哈希
            return Base64.getEncoder().encodeToString(hash);  // Base64编码便于存储
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("SHA-256 algorithm not found", e);  // 理论上不会发生
        }
    }

    /**
     * 获取所有表名的集合。
     * @return 包含所有表名的Set
     */
    public java.util.Set<String> getTableNames() {
        return tableMetadataMap.keySet();
    }
}